<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Spark • Particle Simulation</title>
  <style>
    html, body {
      margin: 0;
      height: 100%;
      width: 100%;
      background-color: black;
    }
  </style>
</head>

<body>
  <script type="importmap">
    {
      "imports": {
        "three": "/examples/js/vendor/three/build/three.module.js",
        "lil-gui": "/examples/js/vendor/lil-gui/dist/lil-gui.esm.js",
        "@sparkjsdev/spark": "/dist/spark.module.js"
      }
    }
  </script>
  <script type="module">
    import * as THREE from "three";
    import {
      SparkRenderer,
      PointerControls,
      SplatMesh,
      generators,
    } from "@sparkjsdev/spark";
    import GUI from "lil-gui";
    import { getAssetFileURL } from "/examples/js/get-asset-url.js";

    const SCENE_CONFIG = {
      SNOW_HEIGHT: 8,
      HALF_WIDTH: 12,
      HALF_DEPTH: 48,
      SNOW_MIN_Y: -4,
      CAMERA_DURATION: 90,
    };

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(
      75,
      window.innerWidth / window.innerHeight,
      0.1,
      1000
    );

    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    const spark = new SparkRenderer({ renderer });
    scene.add(spark);

    function handleResize() {
      const width = window.innerWidth;
      const height = window.innerHeight;
      renderer.setSize(width, height);
      camera.aspect = width / height;
      camera.updateProjectionMatrix();
    }

    handleResize();
    window.addEventListener("resize", handleResize);

    const backgroundURL = await getAssetFileURL("snow-street.spz");
    const background = new SplatMesh({ url: backgroundURL });
    background.quaternion.set(1, 0, 0, 0);
    background.position.set(
      background.position.x - 8,
      background.position.y - 12,
      background.position.z - 104,
    );
    scene.add(background);
    await background.initialized;

    const controls = new PointerControls({ canvas: renderer.domElement });

    const cam = camera.position;
    const snowVolumeBox = new THREE.Box3(
      new THREE.Vector3(
        cam.x - SCENE_CONFIG.HALF_WIDTH,
        cam.y - (SCENE_CONFIG.SNOW_HEIGHT * 8),
        cam.z - SCENE_CONFIG.HALF_DEPTH * 2,
      ),
      new THREE.Vector3(
        cam.x + SCENE_CONFIG.HALF_WIDTH,
        cam.y + SCENE_CONFIG.SNOW_HEIGHT * 2,
        cam.z + SCENE_CONFIG.HALF_DEPTH / 128,
      ),
    );

    const snow = generators.snowBox({
      ...generators.DEFAULT_SNOW,
      box: snowVolumeBox.clone(),
      minY: SCENE_CONFIG.SNOW_MIN_Y,
      color1: new THREE.Color(1.0, 1.0, 1.0),
      color2: new THREE.Color(0.91, 0.9, 0.97),
      density: 1000,
      maxScale: 0.01,
    });
    scene.add(snow.snow);

    const gui = new GUI({ title: "Snow Particle Simulation" });
    const params = {
      fallVelocity: 0.04,
      wanderScale: generators.DEFAULT_SNOW.wanderScale,
      wanderVariance: generators.DEFAULT_SNOW.wanderVariance,
      maxScale: 0.01,

      color1: new THREE.Color(1.0, 1.0, 1.0),
      color2: new THREE.Color(0.91, 0.9, 0.97),

      isPaused: false,
      fallDirection: new THREE.Vector3(0, -1, 0),
      windDirection: "No Direction",

      _previousFallVelocity: generators.DEFAULT_SNOW.fallVelocity,
      _previousWanderScale: generators.DEFAULT_SNOW.wanderScale,
      _previousWanderVariance: generators.DEFAULT_SNOW.wanderVariance,
    };

    // snow parameter controls
    gui.add(params, "fallVelocity", 0.01, 0.5, 0.01).name("Fall Velocity");
    gui.add(params, "wanderScale", 0.01, 1, 0.01).name("Wander Scale");
    gui.add(params, "wanderVariance", 1, 5, 0.1).name("Wander Variance");
    gui.add(params, "maxScale", 0.001, 0.05, 0.001).name("Snow Size");
    gui
      .add(params, "isPaused")
      .name("Pause Snowfall")
      .onChange((value) => {
        if (value) {
          params._previousFallVelocity = params.fallVelocity;
          params._previousWanderScale = params.wanderScale;
          params._previousWanderVariance = params.wanderVariance;

          snow.fallVelocity.value = 0;
          snow.wanderScale.value = 0;
          snow.wanderVariance.value = 0;
        } else {
          snow.fallVelocity.value = params._previousFallVelocity;
          snow.wanderScale.value = params._previousWanderScale;
          snow.wanderVariance.value = params._previousWanderVariance;
        }
      });

    // color
    const colorFolder = gui.addFolder("Snow Colors");
    colorFolder
      .addColor(params, "color1")
      .onChange((value) => {
        snow.color1.value.set(value.r, value.g, value.b);
      })
      .name("Color 1");
    colorFolder
      .addColor(params, "color2")
      .onChange((value) => {
        snow.color2.value.set(value.r, value.g, value.b);
      })
      .name("Color 2");

    // wind
    const directionFolder = gui.addFolder("Wind Orientation");
    directionFolder
      .add(params.fallDirection, "x", -1, 1, 0.1)
      .onChange(() => {
        params.fallDirection.normalize();
        snow.fallDirection.value.copy(params.fallDirection);
      })
      .name("East-West");
    directionFolder
      .add(params.fallDirection, "z", -1, 1, 0.1)
      .onChange(() => {
        params.fallDirection.normalize();
        snow.fallDirection.value.copy(params.fallDirection);
      })
      .name("North-South");

    const windFolder = gui.addFolder("Wind Direction Presets");
    windFolder
      .add(params, "windDirection", [
        "N",
        "NE",
        "E",
        "SE",
        "No Direction",
        "S",
        "SW",
        "W",
        "NW",
      ])
      .onChange((value) => {
        const directions = {
          N: new THREE.Vector3(0, -1, -1),
          NE: new THREE.Vector3(1, -1, -1),
          E: new THREE.Vector3(1, -1, 0),
          SE: new THREE.Vector3(1, -1, 1),
          "No Direction": new THREE.Vector3(0, -1, 0),
          S: new THREE.Vector3(0, -1, 1),
          SW: new THREE.Vector3(-1, -1, 1),
          W: new THREE.Vector3(-1, -1, 0),
          NW: new THREE.Vector3(-1, -1, -1),
        };
        params.fallDirection.copy(directions[value]).normalize();
        snow.fallDirection.value.copy(params.fallDirection);
      })
      .name("Wind Direction");

    let lastTime;
    renderer.setAnimationLoop(function animate(time) {
      const t = time * 0.001;
      const dt = t - (lastTime ?? t);
      lastTime = t;

      controls.update(dt, camera);

      // camera movement
      const targetZ = -13.5;
      const startZ = 0;
      const duration = 90;
      const progress = Math.min(1, t / duration);
      const smoothProgress = Math.sin(progress * Math.PI * 0.5);
      camera.position.z = startZ + (targetZ - startZ) * smoothProgress;

      if (!params.isPaused) {
        snow.fallVelocity.value = params.fallVelocity;
        snow.wanderScale.value = params.wanderScale;
        snow.wanderVariance.value = params.wanderVariance;
        snow.maxScale.value = params.maxScale;
      }

      renderer.render(scene, camera);
    });

  </script>
</body>

</html>
